Z01-05 前端常用-项目:vue3-ts
[TOC]
环境搭建
技术栈
- Vue3:
vue@3.3.2
- TS5:
- Vite4:
vite@4.3.5
。使用create-vue@3.6.4
创建项目 - Pinia2:
pinia@2.1.3
。 - VueRouter4:
vue-router@4.2.2
- Node16:
node@16.19.0
vscode插件
推荐: Vue - Officialv2.0.8
问题: 在Vue - Official
的2.0.x早期版本有问题
解决: 目前版本(v2.0.8
)已解决该问题
废弃插件: Vue Language Features (Volar)
、 TypeScript Vue Plugin (Volar)
问题: 在启用 TypeScript Vue Plugin (Volar)
的情况下,vscode不能识别vue文件的组件返回类型
解决:(暂时)停用TypeScript Vue Plugin (Volar)
插件
项目初始化
创建项目
1、使用create-vue
工具创建mr-vue3-ts-cms
项目。create-vue
是基于vite的脚手架工具
$ pnpm create vue@latest
2、创建选项
目录结构
│ .eslintrc.cjs # eslint检测配置
│ .gitignore # git忽略配置
│ .prettierrc.json # prettier格式化配置
│ env.d.ts # ts声明全局变量的类型定义文件
│ index.html # 模板文件
│ package-lock.json # 包管理
│ package.json # 包管理
│ README.md # 项目文档
│ tsconfig.app.json
│ tsconfig.json # ts编译器的配置文件
│ tsconfig.node.json
│ vite.config.ts # vite配置文件
│
├─.vscode
│ extensions.json # vscode推荐插件
│
├─node_modules
│
├─public
│ favicon.ico
│
└─src
│ App.vue
│ main.ts
│
├─assets
│ ├─css
│ └─img
├─base-ui
├─components
├─hooks
├─router
├─service
├─store
├─utils
└─views
说明: 3个tsconfig文件之间的关系
配置icon,标题
1、配置icon
直接复制自己的icon到public中
2、配置标题
直接在
index.html
模版文件中修改html<!-- index.html --> <title>木头人 - 后台管理</title>
通过JS动态修改
jsdocument.title = '木头人 - 后台管理'
重置CSS样式
1、normalize.css
依赖包: normalize.css
安装:npm i normalize.css
导入: 在main.ts
中导入
import 'normalize.css'
2、reset.less
自定义reset.less重置样式
body, h1, h2, h3, h4, h5, h6, p, dl, dd, ul, ol, li, form, input, textarea, th, td, select, div, section, nav, span, i {
margin: 0;
padding: 0;
box-sizing: border-box;
}
em {
font-style: normal;
}
li {
list-style: none;
}
a {
text-decoration: none;
color: #333;
}
img {
border: none;
vertical-align: top;
}
/* img { font-size: 0; } */
input,
textarea {
outline: none;
}
textarea {
resize: none;
overflow: auto;
}
body {
font-size: 14px;
}
3、common.less
公共样式:common.less
/* 设置主题样式 */
:root {
--el-text-color-placeholder: #d3d5d8 !important;
}
/* 设置公共样式 */
/* 文字溢出省略号 */
.ellipsis-single {
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
white-space: nowrap; // 规定段落中的文本不进行换行
}
.ellipsis-multi {
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
display: -webkit-box; // -webkit-, 作为弹性伸缩盒子模型显示
-webkit-box-orient: vertical; // -webkit-, 设置伸缩盒子的子元素排列方式:从上到下垂直排列
-webkit-line-clamp: 2; // -webkit-, 显示的行数
}
4、vite默认不能识别less
文件,需要安装less
依赖包: less
安装: npm i less -D
vue文件类型声明
问题: 项目本身的vue
模块声明并不能识别出App是一个组件
import App from './App.vue'
解决: 重新声明vue模块,使得ts可以识别出vue是一个组件
// env.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const src: DefineComponent
export default src
}
代码规范
配置editorconfig
.editorconfig
有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。
1、配置项
# http://editorconfig.org
root = true # 当前的配置在根目录中
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行尾的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
2、安装vscode插件:EditorConfig for VS Code
配置Prettier
Prettier
是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。
1、依赖包: prettier
2、安装: npm install prettier -D
3、配置: 配置.prettierrc
或者.prettierrc.json
文件
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "none",
"semi": false
}
说明:
- useTabs:使用tab缩进还是空格缩进,选择false;
- tabWidth:tab是空格的情况下,是几个空格,选择2个;
- printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
- singleQuote:使用单引号还是双引号,选择true,使用单引号;
- trailingComma:在多行输入的尾逗号是否添加,设置为
none
表示不加; - semi:语句末尾是否要加分号,默认值true,选择false表示不加;
4、忽略文件: 创建.prettierignore
忽略文件
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*
5、插件: 安装vscode插件:Prettier - Code formatter
6、测试: 测试prettier是否生效
测试一:在代码中保存代码;
可以通过插件
Prettier - Code formatter
实现测试二:配置一次性修改的命令;
在package.json中配置一个scripts:
sh"prettier": "prettier --write ."
7、自动格式化: 让prettier在保存时自动格式化
- 1 在vscode中安装 Prettier 扩展
- 2 在
设置
中搜索format on save
,选中Editor: Format On Save
- 3 在
设置
中搜索default format
,设置Editor: Default Formatter
为Prettier - Code formatter
- 4 配置
.prettierrc
(见步骤3) - 5 实现保存代码时自动格式化
配置Eslint
1、安装: 在前面创建项目的时候,我们就选择了ESLint,所以Vue会默认帮助我们配置需要的ESLint环境。
2、插件: 安装vscode插件:ESLint
3、问题: 解决eslint和prettier冲突的问题
解决:
1 安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)
- eslint-plugin-prettier(主要)
- eslint-config-prettier
shpnpm i eslint-plugin-prettier eslint-config-prettier -D
2 修改
.eslintrc.cjs
配置jsextends: [ "plugin:vue/vue3-essential", "eslint:recommended", "@vue/typescript/recommended", "@vue/prettier", "@vue/prettier/@typescript-eslint", + // "@vue/eslint-config-prettier/skip-formatting" // 该规范导致eslint没有提示 + '@vue/eslint-config-prettier', + "plugin:prettier/recommended" ],
4、问题: 开发时,会出现一些不需要报错的语法,但是eslint依然报错了
解决: 手动修改eslint检测规则,屏蔽报错
1 需要修改的报错:
@typescript-eslint/no-unused-vars
:未使用的变量名vue/multi-word-component-names
:检测当前的组件名称是否使用驼峰或多单词命名
2 在出现提示的位置,复制出现的错误:
vue/multi-word-component-names
3 在
.eslintrc.cjs
中关闭这些检测规则jsmodule.exports = { + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'vue/multi-word-component-names': 'off' + } }
配置Git
husky
husky
是一个git hook工具,可以帮助我们触发git提交的各个阶段钩子:pre-commit
、commit-msg
、pre-push
痛点: 虽然我们已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了。也就是我们希望保证代码仓库中的代码都是符合eslint规范的。就需要在组员执行 git commit
命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复。
安装:
0、依赖包: husky
1、使用自动配置命令安装husky
# npm
npx husky-init && npm install
# pnpm(推荐)
pnpm dlx husky-init && pnpm install
注意: 在windows的powershell中需要给&&
添加引号:pnpm dlx husky-init '&&' pnpm install
说明: 这里会做三件事:
1 安装husky相关的依赖
2 在项目目录下创建
.husky
文件夹3 在
package.json
中添加一个脚本
2、修改.husky/pre-commit
文件,添加pnpm exec lint
说明: 此时执行git commit
的时候会自动对代码进行lint校验
优化: 暂存区 eslint 校验
由于使用pnpm lint
校验时,会对所有文件都进行校验,耗时久。
为解决以上问题,就出现了 lint-staged
插件,它可以只对有改动的文件进行校验,从而大大优化了检验速度。
1、依赖包: lint-staged
2、安装: pnpm i lint-staged -D
3、配置: 配置lint-staged
1 在
package.json
中配置lint-staged
命令2 配置
lint-staged
方法一:在
.lintstagedrc
文件中配置json{ "*.{js,ts,vue}": "eslint" }
方法二:在
package.json
中配置json"scripts": { ... + "lint-staged": "lint-staged" }, + "lint-staged": { + "*.{js,ts,vue}": [ + "prettier --write", + "eslint" + ] },
3 修改
.husky/pre-commit
文件
4、使用: 通过git commit -m "xxx"
提交git时会使用lint-staged
检测
commitizen
commitizen
是一个帮助我们编写规范 commit message 的工具。
痛点: 通常我们的git commit
会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制。
但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:commitizen
安装:
1、依赖包:
- commitizen
- cz-conventional-changelog
2、安装:
1 安装
commitizen
sh#npm npm install commitizen -D #pnpm pnpm install commitizen -D
2 安装并初始化
cz-conventional-changelog
sh# npm pnpx commitizen init cz-conventional-changelog --save-dev --save-exact # pnpm pnpx commitizen init cz-conventional-changelog --save-dev --save-exact --pnpm
说明: 该命令做了以下事情:
a 帮助安装
cz-conventional-changelog
b 在
package.json
中进行配置
3 在
package.json
中添加scriptsjsonscripts: { "commit": "cz" }
4 提交git时使用以下命令:
shpnpm run commit
问题: 将commitizen
配置在package.json
中时,进行git提交会报错
【补充:报错图片】
解决: 将commitizen
的配置单独写入创建的.czrc
配置文件中
{
"path": "./node_modules/cz-conventional-changelog"
}
提交: 使用commitizen
提交git时的步骤:
- 输入命令:
pnpm run commit
- 第一步是选择type,本次更新的类型
Type | 作用 |
---|---|
feat | 新增特性 (feature) |
fix | 修复 Bug(bug fix) |
docs | 修改文档 (documentation) |
style | 代码格式修改(white-space, formatting, missing semi colons, etc) |
refactor | 代码重构(refactor) |
perf | 改善性能(A code change that improves performance) |
test | 测试(when adding missing tests) |
build | 变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等) |
ci | 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等 |
chore | 变更构建流程或辅助工具(比如更改测试环境) |
revert | 代码回退 |
release | 发布新版本 |
- 第二步选择本次修改的范围(作用域)
? What is the scope of this change (e.g. component or file name): (press enter to skip) git
- 第三步选择提交的信息
? Write a short, imperative tense description of the change (max 89 chars): 安装了husky
- 第四步提交详细的描述信息
? Provide a longer description of the change: (press enter to skip)
- 第五步是否是一次重大的更改
? Are there any breaking changes? (y/N) n
- 第六步是否影响某个open issue
? Does this change affect any open issues? (y/N) n
commitlint
commitlint
是一个 git commit 校验约束工具
就是当我们运行git commmit -m 'xxx'
时,来检查'xxx'
是不是满足团队约定好的提交规范的工具。
安装:
1、依赖包:
- @commitlint/config-conventional
- @commitlint/cli
2、安装: 安装 @commitlint/config-conventional
和 @commitlint/cli
# npm
npm i @commitlint/config-conventional @commitlint/cli -D
# pnpm
pnpm add @commitlint/config-conventional @commitlint/cli -D
3、配置: 在根目录创建commitlint.config.js
文件,配置commitlint
module.exports = {
extends: ['@commitlint/config-conventional']
}
4、使用: 使用husky生成commit-msg文件,验证提交信息:
# npm
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
# pnpm (无效)
pnpx husky add .husky/commit-msg "pnpx --no-install commitlint --edit $1"
问题: commit-msg
中使用pnpm dlx
时,会和--no-install
参数冲突
【补充:报错图片】
解决: 使用npx --no-install
代替 pnpm dlx
问题: vite环境中的.js | .ts
文件不识别module.exports
这种CJS语法
解决: 有2种解决方法:
方法1: 修改
commitlint.config.js
文件后缀为.cjs
,此时就可以解析commonJS代码了方法2:通过快速修复,暂时屏蔽eslint检测,因为这个是误报(该方法在git提交校验时会报错)
js+ // eslint-disable-next-line no-undef module.exports = { extends: ['@commitlint/config-conventional'] }
区分环境
Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量:
- import.meta.env.MODE: {string} 应用运行的模式。(development | production)
- import.meta.env.PROD: {boolean} 应用是否运行在生产环境。
- import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与
import.meta.env.PROD
相反)。 - import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由
base
配置项决定。 - import.meta.env.SSR: {boolean} 应用是否运行在 server 上。
~~方法1:~~手动决定使用哪个BASE_URL
~~方法2:~~根据 import.meta.env.MODE
判断处于哪个环境,使用不同的BASE_URL
方法3: 自定义环境常量
1、创建.env.development
和 .env.production
文件
2、分别在文件中定义不同的常量(注意:常量名必须以VITE_
开头)
3、通过import.meta.env.VITE_XXX
获取定义的常量
第三方库
核心库
vue-router
1、安装vue-router的最新版本:
npm install vue-router@next
2、创建router对象:
import { createRouter, createWebHashHistory } from 'vue-router'
import { RouteRecordRaw } from 'vue-router'
// 映射关系
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/main'
},
{
path: '/main',
component: () => import('../views/main/main.vue')
},
{
path: '/login',
component: () => import('../views/login/login.vue')
}
]
const router = createRouter({
routes,
history: createWebHashHistory()
})
export default router
3、安装router:
import router from './router'
createApp(App).use(router).mount('#app')
4、在App.vue中配置跳转:
<template>
<div id="app">
<router-link to="/login">登录</router-link>
<router-link to="/main">首页</router-link>
<router-view></router-view>
</div>
</template>
pinia
1、安装pinia
npm i pinia
2、创建pinia对象
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
3、挂载pinia
+ import pinia from './store'
const app = createApp(App)
+ app.use(pinia)
app.mount('#app')
4、创建store
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
state: () => ({
counter: 10
}),
getters: {
doubleCounter(state) {
return state.counter * 2
}
},
actions: {
changeCounterAction(payload: number) {
this.counter = payload
}
}
})
export default useCounterStore
5、使用store
获取counter
<template>
<div class="test">
+ <div>计数: {{ counterStore.counter }} - {{ counterStore.doubleCounter }}</div>
</div>
</template>
<script setup lang="ts">
+ import useCounterStore from '@/store/counter'
+ const counterStore = useCounterStore()
</script>
修改counter
<template>
<div class="test">
+ <button @click="setCounter">修改counter</button>
</div>
</template>
<script setup lang="ts">
import useCounterStore from '@/store/counter'
const counterStore = useCounterStore()
// 修改store
+ function setCounter() {
+ counterStore.changeCounterAction(900)
+ }
</script>
vuex(过时)
1、安装vuex:
npm install vuex@next
2、创建store对象:
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
name: 'coderwhy'
}
}
})
export default store
3、安装store:
createApp(App).use(router).use(store).mount('#app')
4、在App.vue中使用:
<h2>{{ $store.state.name }}</h2>
UI库
element-plus
Element Plus,一套为开发者、设计师和产品经理准备的基于 Vue 3.0 的桌面端组件库:
- 相信很多同学在Vue2中都使用过element-ui,而element-plus正是element-ui针对于vue3开发的一个UI组件库;
- 它的使用方式和很多其他的组件库是一样的,所以学会element-plus,其他类似于ant-design-vue、NaiveUI、VantUI都是差不多的;
安装element-plus
npm install element-plus
完整引入
一种引入element-plus的方式是全局引入,代表的含义是所有的组件和插件都会被自动注册:
import { createApp } from 'vue'
+ import ElementPlus from 'element-plus'
+ import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
+ app.use(ElementPlus)
app.mount('#app')
volar支持
如果您使用 Volar,请在 tsconfig.json
中通过 compilerOptions.type
指定全局组件类型。
// tsconfig.json
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
按需引入
也就是在开发中用到某个组件对某个组件进行引入:
<template>
<div id="app">
<router-link to="/login">登录</router-link>
<router-link to="/main">首页</router-link>
<router-view></router-view>
<h2>{{ $store.state.name }}</h2>
<el-button>默认按钮</el-button>
+ <el-button type="primary">主要按钮</el-button>
+ <el-button type="success">成功按钮</el-button>
+ <el-button type="info">信息按钮</el-button>
+ <el-button type="warning">警告按钮</el-button>
+ <el-button type="danger">危险按钮</el-button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
+ import { ElButton } from 'element-plus'
export default defineComponent({
name: 'App',
+ components: {
+ ElButton
+ }
})
</script>
<style lang="less">
</style>
但是我们会发现是没有对应的样式的,引入样式有两种方式:
- 全局引用样式(像之前做的那样);
- 局部引用样式(通过babel的插件);
1.安装babel的插件:
npm install babel-plugin-import -D
2.配置babel.config.js
module.exports = {
plugins: [
[
'import',
{
libraryName: 'element-plus',
customStyleName: (name) => {
return `element-plus/lib/theme-chalk/${name}.css`
}
}
]
],
presets: ['@vue/cli-plugin-babel/preset']
}
但是这里依然有个弊端:
- 这些组件我们在多个页面或者组件中使用的时候,都需要导入并且在components中进行注册;
- 所以我们可以将它们在全局注册一次;
import {
ElButton,
ElTable,
ElAlert,
ElAside,
ElAutocomplete,
ElAvatar,
ElBacktop,
ElBadge,
} from 'element-plus'
+ const app = createApp(App)
const components = [
ElButton,
ElTable,
ElAlert,
ElAside,
ElAutocomplete,
ElAvatar,
ElBacktop,
ElBadge
]
+ for (const cpn of components) {
+ app.component(cpn.name, cpn)
+ }
自动按需引入(推荐)
首先你需要安装unplugin-vue-components
和 unplugin-auto-import
这两款插件
npm install unplugin-vue-components unplugin-auto-import -D
然后把下列代码插入到你的 Vite
或 Webpack
的配置文件中
Vite
1、设置vite.config.ts
,添加插件Components
和Components
// vite.config.ts
import { defineConfig } from 'vite'
+ import AutoImport from 'unplugin-auto-import/vite'
+ import Components from 'unplugin-vue-components/vite'
+ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
+ plugins: [
// ...
+ AutoImport({
+ resolvers: [ElementPlusResolver()],
++ dts: 'auto-imports.d.ts' // 重点
+ }),
+ Components({
+ resolvers: [ElementPlusResolver()],
++ dts: 'components.d.ts' // 重点
+ }),
],
})
2、修改tsconfig.app.json
,添加"auto-imports.d.ts", "components.d.ts"
到include
中
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"], // 重点
"exclude": ["src/**/__tests__/*", "commitlint.config.js"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Webpack
// webpack.config.js
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
module.exports = {
// ...
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
}
类型提示设置
在tsconfig.json
中将安装的2个插件对应的类型是声明文件添加到include
中
vant
安装
安装: 推荐使用 按需自动导入 的方式安装vant
0、 依赖包:
- 核心包
- vant
- 自动导入插件
- @vant/auto-import-resolver
- unplugin-vue-components
- unplugin-auto-import
1、安装:pnpm add vant
2、导入样式: 在main.ts
中添加vant样式(最新版已经不需要该操作)
import 'vant/lib/index.css'
3、按需自动导入:(新版:2024-5-29)
1 安装自动导入插件
sh# 通过 pnpm 安装 pnpm add @vant/auto-import-resolver unplugin-vue-components unplugin-auto-import -D
2 配置插件
如果是基于 Vite 的项目,在
vite.config.js
文件中配置插件tsimport vue from '@vitejs/plugin-vue'; + import AutoImport from 'unplugin-auto-import/vite'; + import Components from 'unplugin-vue-components/vite'; + import { VantResolver } from '@vant/auto-import-resolver'; export default { plugins: [ vue(), + AutoImport({ + resolvers: [VantResolver()], }), + Components({ + resolvers: [VantResolver()], }), ], };
4、配置vant组件类型提示: 修改tsconfig.app.json
,添加"auto-imports.d.ts", "components.d.ts"
到include
中
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"], // 重点
"exclude": ["src/**/__tests__/*", "commitlint.config.js"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
5、使用: 使用组件和API
<!-- 使用组件 -->
<template>
<van-button type="primary" />
</template>
<!-- 使用API -->
<script>
showToast('No need to import showToast');
</script>
说明: 可以直接在模板中使用 Vant 组件,unplugin-vue-components
会解析模板并自动注册对应的组件, @vant/auto-import-resolver
会自动引入对应的组件样式。
补充:
按需自动导入:(旧版)
依赖包: unplugin-vue-components
1 安装插件
shpnpm i unplugin-vue-components -D
2 配置插件
js// vite.config.js import vue from '@vitejs/plugin-vue'; + import Components from 'unplugin-vue-components/vite'; + import { VantResolver } from 'unplugin-vue-components/resolvers'; export default { plugins: [ vue(), + Components({ + resolvers: [VantResolver()], + }), ], };
3 使用组件
html<template> <van-button type="primary" /> </template>
工具库
axios
1、安装axios
npm install axios
2、封装axios
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { Result } from './types'
import { useUserStore } from '/@/store/modules/user'
class HYRequest {
private instance: AxiosInstance
private readonly options: AxiosRequestConfig
constructor(options: AxiosRequestConfig) {
this.options = options
this.instance = axios.create(options)
this.instance.interceptors.request.use(
(config) => {
const token = useUserStore().getToken
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(err) => {
return err
}
)
this.instance.interceptors.response.use(
(res) => {
// 拦截响应的数据
if (res.data.code === 0) {
return res.data.data
}
return res.data
},
(err) => {
return err
}
)
}
request<T = any>(config: AxiosRequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
this.instance
.request<any, AxiosResponse<Result<T>>>(config)
.then((res) => {
resolve((res as unknown) as Promise<T>)
})
.catch((err) => {
reject(err)
})
})
}
get<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.request({ ...config, method: 'GET' })
}
post<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.request({ ...config, method: 'POST' })
}
patch<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.request({ ...config, method: 'PATCH' })
}
delete<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.request({ ...config, method: 'DELETE' })
}
}
export default HYRequest
postcss-px-to-viewport
postcss-px-to-viewport
是将px单位转换为视口单位 (vw, vh, vmin, vmax) 的 PostCSS 插件
官网: https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
安装:
依赖包:
- postcss-px-to-viewport(废弃)
- postcss-px-to-viewport-8-plugin
1、安装: pnpm i postcss-px-to-viewport -D
2、配置: 在postcss.config.cjs
添加如下配置
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
'postcss-px-to-viewport': {
// options
viewportWidth: 375 // 设备宽度375计算vw的值
}
}
}
注意: 在配置 postcss-loader 时,应避免 ignore node_modules 目录,否则将导致 Vant 样式无法被编译
问题: postcss-px-to-viewport
插件已经废弃,控制台会报警告:
解决: 使用postcss-px-to-viewport-8-plugin
代替
1、安装: pnpm i postcss-px-to-viewport-8-plugin -D
2、配置: 在postcss.config.cjs
添加如下配置
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
// 'postcss-px-to-viewport': {
'postcss-px-to-viewport-8-plugin': {
// options
viewportWidth: 375 // 设备宽度375计算vw的值
}
}
}
pinia-plugin-persistedstate
pinia-plugin-persistedstate
丰富的功能可以使 Pinia Store 的持久化更易配置。本插件兼容 pinia^2.0.0
。
官网: https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/
安装:
依赖包: pinia-plugin-persistedstate
1、安装: pnpm i pinia-plugin-persistedstate
2、配置: 将插件添加到 pinia 实例上
import { createPinia } from 'pinia'
+ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
+ pinia.use(piniaPluginPersistedstate)
3、使用: 创建 Store 时,将 persist
选项设置为 true
// 组合式语法
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useStore = defineStore(
'main',
() => {
const someState = ref('你好 pinia')
return { someState }
},
{
+ persist: true,
},
)
// 选项式语法
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => {
return {
someState: '你好 pinia',
}
},
+ persist: true,
})
vite-plugin-svg-icons
vite-plugin-svg-icons
用于生成 svg 雪碧图。根据 icons 文件svg图片打包到项目中,通过组件使用图标
安装:
依赖包: vite-plugin-svg-icons
1、安装: pnpm i vite-plugin-svg-icons -D
2、配置: 在vite.config.ts
中的配置插件
+ import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig({
plugins: [
vue(),
+ createSvgIconsPlugin({
+ // 指定图标文件夹,绝对路径(NODE代码)
+ iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')]
})
],
3、导入到main.ts
+ import 'virtual:svg-icons-register'
4、使用: 使用svg精灵地图
<svg aria-hidden="true">
<!-- #icon-文件夹名称-图片名称 -->
<use href="#icon-login-eye-off" />
</svg>
封装svg组件
1、定义全局组件CpIcon
<script setup lang="ts">
// 提供name属性即可
defineProps<{
name: string
}>()
</script>
<template>
<svg aria-hidden="true" class="cp-icon">
<use :href="`#icon-${name}`" />
</svg>
</template>
<style lang="scss" scoped>
.cp-icon {
// 和字体一样大
width: 1em;
height: 1em;
}
</style>
2、定义CpIcon
组件类型
import CpNavBar from '@/components/CpNavBar.vue'
+ import CpIcon from '@/components/CpIcon.vue'
declare module 'vue' {
interface GlobalComponents {
CpNavBar: typeof CpNavBar
+ CpIcon: typeof CpIcon
}
}
3、使用组件
<van-field v-model="password" placeholder="请输入密码">
<template #button>
+ <cp-icon :name="login-eye-off"></cp-icon>
</template>
</van-field>
nprogress
ajax请求的进度条
使用文档:https://juejin.cn/post/7136118122867752990#heading-11
安装
依赖包: nprogress
1、安装:
# 导入nprogress
npm i nprogress
# 导入TS类型
pnpm add @types/nprogress -D
2、引入:
import NProgress from "nprogress"
import "nprogress/nprogress.css"
常见用法: 常见的用法是在 路由导航守卫 或 拦截器 中使用
路由导航守卫
jsrouter.beforeEach(async(to, from, next) => { + NProgress.start(); } router.afterEach(() => { + NProgress.done(); });
拦截器
js//请求拦截器 instance.interceptors.request.use( (config) => { + NProgress.start(); //... }, (error) =>{ //... } ); //响应拦截器 instance.interceptors.response.use( (response) => { //响应成功 + NProgress.done(); //... }, (error) => { + NProgress.done(); //... } );
常用API
- NProgress.configure(opts):
opts
,配置项- showSpinner:
boolean
,默认:true
,控制是否显示进度条右下方加载的小圆圈动画 - easing:
ease | ease-out |...
,默认:ease
,CSS 缓动字符串 - speed:
number
,默认:200
,动画速度,单位ms - minimum:
number
,默认:0.08
,更改启动时使用的最小百分比 - parent:
CSS选择器
,默认:body
,指定此选项可更改父容器
- showSpinner:
修改样式
/* common.scss */
/* 修改NProgress样式 */
#nprogress .bar {
background: var(--cp-primary) !important;
}
vue-use
文档:https://vueuse.org/functions.html#category=State
组合式Vue的工具函数集
安装
1、依赖包: @vueuse/core
2、安装: npm i @vueuse/core
常用API
- Elements
- useWindowSize():
,获取实时的window尺寸。原生:
window.innerWidth
- 返回值:
{width, height}
,返回window的尺寸- width:
,
默认:
,window宽度 - height:
,
默认:
,window高度
- width:
- 返回值:
基本使用
socket.io-client【】
Socket.IO: 是一个库,可以在客户端和服务器之间实现 低延迟, 双向 和 基于事件的 通信。
常用API
基本使用
echarts
API
- echarts
echarts.init(dom, theme?, opt?)
echarts.registerMap(mapName, opt | geoJSON)
- echartsInstance
echartsInstance.setOption()
安装
依赖包: echarts
安装: pnpm add echarts
基本使用
1、指定echarts的容器
2、引入echarts
封装Echarts
1、目录结构
组件:BaseEchart
1、页面布局
2、使用组件
组件:PieEchart
1、使用组件
2、使用base-echart
组件,并传入option
3、option配置
4、修改BaseEchart,使用传递进来的option
5、统一导出
组件:LineEchart
1、使用组件
2、配置
3、请求商品销量数据
见:动态渲染数据
4、转化请求的商品销量数据
5、传递数据
组件:RoseEchart
1、使用组件
2、页面布局
3、修改BaseEchart
组件:BarEchart
1、store
2、组件
3、渲染数据
4、配置
组件:MapEchart
1、注册map
2、页面布局
3、获取城市经纬度工具函数
4、经纬度数据
常见功能
TS类型【】
全局类型【】
如果定义的类型,在views、store、service等多个地方都会用到,就放入src/types
中
StoreState
定义store中的state的类型
+ interface ILoginState {
+ token: string
+ userInfo: any
+ roleMenuTreeInfo: any
+ }
const useLoginStore = defineStore('login', {
+ state: (): ILoginState => ({
token: localCache.getCache('token') ?? '',
userInfo: {},
roleMenuTreeInfo: {}
}),
})
icon
导入icon
0、依赖包: @element-plus/icons-vue
1、安装图标:npm install @element-plus/icons-vue
2、全局注册图标:
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
3、对上面的方法进行封装:
使用
import registerIcons from './global/registerIcons'
const app = createApp(App)
+ app.use(registerIcons)
封装
// 注册element-plus图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
function registerIcons(app: App<Element>) {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
}
export default registerIcons
4、在label插槽中添加图标
<!-- 登录表单 -->
<el-tabs v-model="activeName" class="tabs" type="border-card" stretch>
<el-tab-pane label="帐号登录" name="account">
+ <template #label>
<div class="label">
+ <el-icon><UserFilled /></el-icon>
+ <span class="text">帐号登录</span>
</div>
</template>
</el-tab-pane>
<el-tab-pane label="手机登录" name="phonse">
+ <template #label>
<div class="label">
+ <el-icon><Iphone /></el-icon>
+ <span class="text">手机登录</span>
</div>
</template>
</el-tab-pane>
</el-tabs>
icon地图
进度条
nprogress
本地缓存
封装Cache
1、本地缓存-基本使用
const useLoginStore = defineStore('login', {
state: () => ({
+ token: localStorage.getItem('token') ?? ''
}),
actions: {
async loginAction(account: IAccount) {
// 发送网络请求
const loginRes = await loginRequest(account)
// 保存请求数据到store
this.token = loginRes.data.token
// 保存请求数据到本地
+ localStorage.setItem('token', this.token)
}
}
})
2、本地缓存-封装
在utils/cache/index.ts
中封装操作LocalStorage的类Cache
class Cache {
setCache(key: string, value: any) {
if (value) {
localStorage.setItem(key, JSON.stringify(value))
}
}
getCache(key: string) {
const value = localStorage.getItem(key)
if (value) {
return JSON.parse(value)
}
}
removeCache(key: string) {
localStorage.removeItem(key)
}
clear() {
localStorage.clear()
}
}
export default new Cache()
3、本地缓存-封装-兼容localStorage、sessionStorage
enum TCache {
Local,
Session
}
class Cache {
storage: Storage
constructor(type: TCache) {
this.storage = type === TCache.Local ? localStorage : sessionStorage
}
setCache(key: string, value: any) {
if (value) {
this.storage.setItem(key, JSON.stringify(value))
}
}
getCache(key: string) {
const value = this.storage.getItem(key)
if (value) {
return JSON.parse(value)
}
}
removeCache(key: string) {
this.storage.removeItem(key)
}
clear() {
this.storage.clear()
}
}
const localCache = new Cache(TCache.Local)
const sessionCache = new Cache(TCache.Session)
export { localCache, sessionCache }
4、本地缓存-封装-使用
const useLoginStore = defineStore('login', {
state: () => ({
+ token: localCache.getCache('token') ?? ''
}),
actions: {
async loginAction(account: IAccount) {
// 发送网络请求
const loginRes = await loginRequest(account)
// 保存请求数据到store
this.token = loginRes.data.token
// 保存请求数据到本地
+ localCache.setCache('token', this.token)
}
}
})
Store持久化
记住密码
思路: 记住密码状态isRemPwd
在父组件中,而帐号和密码在子组件中,可以将isRemPwd
通过loginAction(isRemPwd)
传递给子组件
1、传递isRemPwd到子组件
function loginClickHdl() {
if (activeName.value === 'account') {
// 调用PaneAccount组件内部方法
+ accountRef.value?.loginHdl(isRemPwd.value) // isRemPwd.value
} else {
console.log('通过手机登录')
}
}
2、登录成功后缓存帐号、密码
function loginHdl(isRemPwd: boolean) {
// 登录前校验
accountRef.value?.validate((valid) => {
if (valid) {
loginStore.loginAction({ name: account.name, password: account.password }).then(() => {
// 登录成功,记住密码
if (isRemPwd) {
+ localCache.setCache('name', account.name)
+ localCache.setCache('password', account.password)
}
})
} else {
ElMessage.error('呜~, 校验失败,请输入正确的账号密码格式~')
}
})
}
3、修改初始化帐号、密码
const account = reactive({
+ name: localCache.getCache('name') ?? '',
+ password: localCache.getCache('password') ?? ''
})
4、未勾选记住密码时,移除缓存
// 登录成功,记住密码
if (isRemPwd) {
localCache.setCache('name', account.name)
localCache.setCache('password', account.password)
} else {
+ localCache.removeCache('name')
+ localCache.removeCache('password')
}
5、缓存记住密码状态
在父组件中watch监听isRemPwd的值
/* 缓存记住密码状态 */
const isRemPwd = ref<boolean>(localCache.getCache('isRemPwd') ?? false)
watch(isRemPwd, (newValue) => {
localCache.setCache('isRemPwd', newValue)
})
token
携带token
1、*方法一:*在每次请求中添加headers.Authorization
/* 获取用户详细信息 */
export function getUserInfo(id: number) {
return mrRequest.get({
url: `/users/${id}`
+ headers: {
+ Authorization: 'Bearer ' + localCache.getCache('token')
+ }
})
}
2、方法二: 在拦截器中添加headers.Authorization
(推荐)
const mrRequest = new MrRequest({
baseURL: BASE_URL,
timeout: TIME_OUT,
interceptors: {
requestSuccessFn: (config) => {
// 携带token
+ if (config.headers) {
+ config.headers.Authorization = 'Bearer ' + localCache.getCache('token')
+ }
return config as InternalAxiosRequestConfig
},
...
}
})
表单校验
校验规则
校验规则选项
type:指定输入数据的类型
required:表示是否必填,值为 true 或 false。
message:表示校验不通过时的提示消息。
trigger:表示触发校验的事件类型,可以是 blur、change 等。
min 和 max:分别表示输入值的最小值和最大值。例如 min:3 表示输入的值必须大于等于 3。
pattern:表示输入值的正则表达式,例如 pattern:/^[a-z]+$/i 表示输入的值必须由字母构成。
len:表示输入值的长度,例如 len:6 表示输入的值必须恰好为 6 个字符。
工具函数
时间格式化
依赖包:dayjs
1、安装
npm i dayjs
2、封装dayjs
import dayjs from 'dayjs'
+ import utc from 'dayjs/plugin/utc'
// 继承utc
+ dayjs.extend(utc)
export function formatUTC(utcString: string, format: string = 'YYYY-MM-DD HH:mm:ss') {
+ return dayjs.utc(utcString).format(format)
}
3、转成东八区时间
export function formatUTC(utcString: string, format: string = 'YYYY-MM-DD HH:mm:ss') {
+ return dayjs.utc(utcString).utcOffset(8).format(format)
}
4、使用封装的方法进行格式化
<script setup lang="ts">
+ import { formatUTC } from '@/utils/format
</script>
<el-table-column align="center" prop="createAt" label="创建时间">
<template #default="scope">{{ formatUTC(scope.row.createAt) }}</template>
</el-table-column>
<el-table-column align="center" prop="updateAt" label="更新时间">
<template #default="scope">{{ formatUTC(scope.row.updateAt) }}</template>
</el-table-column>
map函数【】
element-plus
element国际化
方法一: 全局引入
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
app.use(ElementPlus, {
locale: zhCn,
})
方式二: 自动按需引入(推荐)
<template>
<div class="app">
+ <el-config-provider :locale="zhCn">
<RouterView />
</el-config-provider>
</div>
</template>
<script setup lang="ts">
+ import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>
添加.mjs
文件声明
declare module '*.mjs'
效果:
动画
数据动画
1、动画插件:npm i countup.js
2、使用countup添加动画
3、添加人民币符号
权限管理
权限管理系统-RBAC
RBAC (Role-Based Access Control) 即基于角色的访问控制,是一种常见的访问控制机制,用于管理用户和资源之间的访问权限。RBAC 基于角色对用户进行授权,而不是直接授予用户特定的权限集合。通过这种方式,RBAC 可以使权限管理更易于管理和扩展。
在 RBAC 中,有三个核心概念:用户、角色和权限。其中:
- 用户:系统中的每个用户都有唯一的标识符,可以被分配到一个或多个角色。
- 角色:每个角色代表了一组权限集合,包含了访问系统中的某些资源所需的权限。角色可以被分配给一个或多个用户。
- 权限:权限是指访问系统中某些资源所需的能力,例如读取、写入或执行某些操作等。
通过将用户分配到角色,并为每个角色分配适当的权限,RBAC 可以简化权限管理过程,减少管理员管理的工作量,同时也可以帮助保证系统安全性。
页面访问权限控制
1、在router/index.ts中添加导航守卫
/* 路由导航守卫 */
router.beforeEach((to, from) => {
const token = localCache.getCache('token')
if (to.path === '/main' && !token) {
return '/login'
}
})
动态路由菜单【】
分配权限
新建角色时分配权限
定义配置
formItems: [
{ type: 'input', label: '角色名称', prop: 'name', placeholder: '请输入角色名称' },
{ type: 'input', label: '权限介绍', prop: 'intro', placeholder: '请输入权限介绍' },
+ { type: 'custom', label: '分配权限', slotName: 'menuList' }
]
自定义插槽
<template v-else-if="item.type === 'custom'">
<slot :name="item.slotName"></slot>
</template>
请求完整菜单树数据
1、service
/* 获取权限列表 */
export function postMenuLists() {
return mrRequest.post({
url: '/menu/list'
})
}
2、store
async postMenuListsAction() {
const res = await postMenuLists()
this.menuLists = res.data.list
}
3、组件Role中
const mainStore = useMainStore()
const { menuLists } = storeToRefs(mainStore)
const defaultProps = {
children: 'children',
label: 'name'
}
<PageModal :modal-config="modalConfig" :other-info="otherInfo" ref="modalRef">
+ <template #menuList>
<el-tree
+ :data="menuLists"
+ show-checkbox
+ node-key="id"
+ :props="defaultProps"
ref="treeRef"
@check="hdlSelectChecked"
/>
</template>
</PageModal>
创建角色时带权限
1、点击权限树后获取选中项的id
<el-tree
:data="menuLists"
show-checkbox
node-key="id"
:props="defaultProps"
ref="treeRef"
+ @check="hdlSelectChecked"
/>
/* 获取选中的权限节点 */
const otherInfo = ref({})
function hdlSelectChecked(param1: any, param2: any) {
const menuList = [...param2.checkedKeys, ...param2.halfCheckedKeys]
otherInfo.value = { menuList }
}
hdlSelectChecked
的2个参数:
2、传递额外的数据otherInfo到PageModal组件中
<PageModal :modal-config="modalConfig"
+ :other-info="otherInfo"
ref="modalRef">
3、在PageModal组件中接收数据
export interface IModalProps {
modalConfig: IModalConfig
+ otherInfo?: any
}
const props = defineProps<IModalProps>()
4、合并otherInfo和formData
function hdlSubmitUser() {
modalVisiable.value = false
+ pageInfo.value = { ...pageForm, ...props.otherInfo }
console.log('pageInfo', pageInfo.value)
// 验证表单
formRef.value?.validate((valid: any) => {
if (valid) {
// 验证成功
if (isEdit.value) {
+ systemStore.editPageAction(props.modalConfig.pageName, pageId.value, pageInfo.value)
ElMessage.success('哈哈,修改用户成功~')
} else {
+ systemStore.addPageAction(props.modalConfig.pageName, pageInfo.value)
ElMessage.success('哈哈,新增用户成功~')
}
} else {
// 验证失败
ElMessage.error('呜呼,验证失败,请重新来过~')
}
})
}
权限菜单回显
1、绑定ElTree的ref
<el-tree
:data="menuLists"
show-checkbox
node-key="id"
:props="defaultProps"
+ ref="treeRef"
@check="hdlSelectChecked"
/>
2、在Role组件中定义回调函数,并将其传递给usePageModal中,目的是为了获取数据itemData
+ const { modalRef, hdlChangeVisiable, hdlEditClick } = usePageModal(editCB)
/* 点击编辑,回显权限 */
const treeRef = ref<InstanceType<typeof ElTree>>()
+ function editCB(pageItem: any) {
// console.log('pageItem: ', pageItem.menuList)
nextTick(() => {
const checkedKeys = mapMenuListToIds(pageItem.menuList)
treeRef.value?.setCheckedKeys(checkedKeys)
})
}
3、在Hook中接收回调函数
function usePageModal(editCB: (pageItem: any) => void) {
/* 修改对话框是否显示 */
// const modalRef = ref<InstanceType<typeof PageModal>>()
const modalRef = ref<any>()
function hdlChangeVisiable() {
modalRef.value?.changeModalVisiable()
}
/* 调用模态组件内函数,修改用户 */
function hdlEditClick(pageItem: any) {
modalRef.value?.changeModalVisiable(pageItem)
+ if (editCB) editCB(pageItem)
}
return {
modalRef,
hdlChangeVisiable,
hdlEditClick
}
}
4、定义一个获取数组中id的工具函数
/**
* 根据权限列表获取其所有根节点id
* @param menuList 权限列表
* @returns 权限列表的所有根节点id
*/
export function mapMenuListToIds(menuList: any[]) {
const ids: number[] = []
function recurseGetId(menuList: any[]) {
for (const item of menuList) {
if (item.children) {
recurseGetId(item.children)
} else {
ids.push(item.id)
}
}
}
recurseGetId(menuList)
console.log('ids', ids)
return ids
}
5、使用获取到的id,调用ElTree的setCheckedKeys
方法,给控件添加初始值
/* 点击编辑,回显权限 */
const treeRef = ref<InstanceType<typeof ElTree>>()
function editCB(pageItem: any) {
// console.log('pageItem: ', pageItem.menuList)
+ nextTick(() => {
+ const checkedKeys = mapMenuListToIds(pageItem.menuList)
+ treeRef.value?.setCheckedKeys(checkedKeys)
})
}
新增重置权限菜单
思路: 在新增角色时,同样添加一个callback,并在setCheckedKeys
时传递一个空数组
按钮权限
获取用户所有按钮权限
1、定义获取userMenus中所有permissions的工具函数
2、使用工具函数,在@store/login/login.ts
中的loginAction
和loadLocalCacheAction
中都获取permissions
根据权限展示按钮
1、在PageContent中获取对应的增删改查的权限
2、根据权限展示、隐藏按钮
3、封装权限判断
4、使用封装的hook
5、如果没有query权限,不展示PageSearch组件
PageSearch.vue